﻿// FILE           :frmOrbs.cs
// PROJECT        :SETCritters                                        
// PROGRAMMER     :Kenneth Jinks and Ryan Pink                                         
// FIRST VERSION  :November 26 2013                                                
// DESCRIPTION    :
//      This file contains the meat and potatoes of the form application. It processes the game loop and controls
//      the entities (orbs and critters) that are being rendered to the screen. The 'd' button clears the orbs
//      from the screen. There are debug labels rendered to the screen for vital rendering information.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Orbs2Revengeance
{
    /// <summary>
    ///     This struct represents a newly assigned position to a given orb.
    /// </summary>
    public struct OrbPosition
    {
        public int id;
        public int x;
        public int y;
    }

    public partial class frmOrbs : Form
    {
        const int FRAME_RATE = 32;

        static Orb newOrb = null;       // a new orb being added to the game loop
        static OrbPosition newMove;     // a new orb vector being added to the game loop
        static int newPulse = -1;       // a new orb pulse request
        static int newCatch = -1;       // a new critter catch request

        List<Orb> theOrbs = new List<Orb>();    // All orbs in the environment
        Color bgColor = new Color();

        List<ThePuppeteer.Puppeteer> puppeteers = new List<ThePuppeteer.Puppeteer>();   // All critter animation controllers
        List<CritterType> theCritters = new List<CritterType>();                        // All critter data containers
        int critterFrame = 0;           // Current animation state for critters

        TheServer.Server listener = new TheServer.Server(5001);     // Client listener thread
        TcpClient client = new TcpClient();                         // used by the listener

        /// <summary>
        /// public CritterType GetClosestCritter(Point cursor)
        /// </summary>
        /// <param name="cursor">
        /// The position to find a close critter.
        /// </param>
        /// <returns>
        /// CritterType - the struct containing the details of the closest critter
        /// </returns>
        public int GetClosestCritter(Point cursor)
        {
            int closestDistance = Int32.MaxValue;
            
            int critterIndex = 0;

            for (int i = 0; i < puppeteers.Count; i++)
            {
                int distance = (int)Math.Sqrt(Math.Pow(puppeteers[i].PuppetPosition.X - cursor.X, 2) +  // calculate vector
                                              Math.Pow(puppeteers[i].PuppetPosition.Y - cursor.Y, 2));

                if (distance < closestDistance)     // adjust current closest critter
                {
                    critterIndex = i;
                    
                    closestDistance = distance;
                    Console.WriteLine("index: " + i + " :: distance: " + closestDistance + " :: (x,y): " + cursor.X + ", " + cursor.Y);
                }
            }

            return critterIndex;
        }

        public frmOrbs()
        {
            InitializeComponent();

            InitGame();
        }


        /// <summary>
        ///     This method initializes the game components, starts the listener thread, and starts the game loop timer.
        /// </summary>
        private void InitGame()
        {
            Thread listenerThread = new Thread(StartListener);  // start the listener thread
            listenerThread.Name = "ListenerThread";
            listenerThread.Start();

            newMove.id = 0;     // initialize the member newMove struct
            
            InitPuppeteer();    // initialize the puppeteers for the critters
            
            picOrbs.Image = new Bitmap(picOrbs.Width, picOrbs.Height);          // initialize the environment
            PopulateOrbs();
            picOrbs.BackgroundImage = new Bitmap(picOrbs.Width, picOrbs.Height);

            this.KeyPress += new KeyPressEventHandler(SpawnOrb);    // add another orb via spacebar
            bgColor = Color.FromArgb(26, Color.Black);
            
            System.Windows.Forms.Timer tmr = new System.Windows.Forms.Timer();  // initialize game timer to 32 FPS
            tmr.Interval = (1000 / FRAME_RATE);
            tmr.Tick += new EventHandler(GameLoop);

            tmr.Start();
        }


        /// <summary>
        ///     This is the thread start method for the listener data member. It sits on a wait
        ///     until a client attempts to connect.
        /// </summary>
        private void StartListener()
        {
            Console.WriteLine("Server has entered listening loop");

            while (true)
            {
                listener.WaitForClientConnect(ref client);
            }

            client.Close();
        }


        /// <summary>
        ///     This method initializes the critter puppeteers and populates the
        ///     environment with a base number of critters (in this case, hard-coded
        ///     as 24.
        /// </summary>
        private void InitPuppeteer()
        {
            for (int i = 0; i < 24; ++i)
            {
                #region CritterData
                CritterType critterData = new CritterType();
                Random r = new Random(DateTime.Now.Millisecond + i);

                int scale = 2;
                critterData.curve = 0.0f;

                float phase = (float)(r.NextDouble() * 2.0f * Math.PI);

                int red = (int)(Math.Sin(6.2832f + phase) * 127.0f) + 128;
                int green = (int)(Math.Sin(2.0944f + phase) * 127.0f) + 128;
                int blue = (int)(Math.Sin(4.1888f + phase) * 127.0f) + 128;

                critterData.foreColour = Color.FromArgb(red/2, green / 2, blue / 2);
                critterData.backColour = Color.FromArgb(red, green, blue);

                critterData.headHeight = r.Next(0, 60/scale);
                critterData.headAngle = 0;

                int armLength = r.Next(0, 80/scale);
                double armAngle = 0;

                critterData.leftArmLength = armLength;
                critterData.leftArmAngle = (float)armAngle;

                critterData.rightArmLength = armLength;
                critterData.rightArmAngle = (float)armAngle;

                critterData.bodyHeight = (float)(r.NextDouble()*60/scale + 20);//11
                critterData.bodyWidth = (float)(r.NextDouble() * 60/scale + 20);//12

                critterData.waistWidth = (float)(r.NextDouble() * 60/scale + 20);//13

                int legLength = r.Next(0, 80/scale);
                double legAngle = 0;

                critterData.leftLegLength = legLength;//14 15
                critterData.leftLegAngle = (float)legAngle;//16 17

                critterData.rightLegLength = legLength;
                critterData.rightLegAngle = (float)legAngle;
                #endregion


                puppeteers.Add(new ThePuppeteer.Puppeteer());
                theCritters.Add(critterData);
            }
        }


        /// <summary>
        ///     This method handles keypresses on the server side. If the server detects
        ///     a keypress, it will either populate the environment with a new orb or
        ///     clear the existing orbs from the list.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SpawnOrb(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == ' ')       // Add another orb
            {
                Random r = new Random(DateTime.Now.Millisecond);

                Orb newOrb = new Orb(Color.FromArgb(r.Next(0, 256), r.Next(0, 256), r.Next(0, 256)), r.Next(52, 64));

                Point newPnt = new Point(r.Next(0, 10) - 4, r.Next(0, 10) - 4);
                newOrb.Velocity = newPnt;
                newPnt = new Point(r.Next(0, 130) * 10, r.Next(0, 70) * 10);
                newOrb.Position = newPnt;

                theOrbs.Add(newOrb);

                lblSpecs.Text = "Orbs: " + theOrbs.Count;
            }
            else if (e.KeyChar == 'd')  // Clear all the orbs
            {
                theOrbs.Clear();
            }
        }


        /// <summary>
        ///     This method is a test function to populate the screen with a base number of
        ///     smaller orbs that wander randomly on their own. I've decided to leave them
        ///     in to add a bit of variety and activity to the environment.
        /// </summary>
        private void PopulateOrbs()
        {
            Random r = new Random(DateTime.Now.Millisecond);

            for (int i = 0; i < 24; ++i)
            {
                Orb newOrb = new Orb(Color.FromArgb(r.Next(0, 256), r.Next(0, 256), r.Next(0, 256)), r.Next(28, 40));

                Point newPnt = new Point(r.Next(0, 10) - 4, r.Next(0, 10) - 4);
                newOrb.Velocity = newPnt;

                newPnt = new Point(r.Next(0, 10) * 100, r.Next(0, 8) * 100);
                newOrb.Position = newPnt;

                theOrbs.Add(newOrb);
            }
        }


        /// <summary>
        ///     This method is the event handler function that is called for every
        ///     timer tick. All it does is call the Reshape() and Render() methods
        ///     in respective order.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GameLoop(object sender, EventArgs e)
        {
            Reshape();
            Render();
        }


        /// <summary>
        ///     This method performs most of the data manipulation for each tick of the
        ///     game loop. It adjusts positions and velocities, processes new orbs, and
        ///     adjusts the eye-tracking of the critters.
        /// </summary>
        private void Reshape()
        {
            if (newOrb != null)     // There's a new orb! Add them to the list
            {
                theOrbs.Add(newOrb);

                newOrb = null;
            }

            if (newMove.id != 0)    // Someone wants to change trajectory! Make the change.
            {
                for (int i = 0; i < theOrbs.Count; ++i)
                {
                    if (theOrbs[i].ID == newMove.id)
                    {
                        theOrbs[i].Velocity = new Point(newMove.x, newMove.y);
                        break;
                    }
                }

                newMove.id = 0;
            }

            double velX = 0;
            double velY = 0;

            for (int i = 0; i < theOrbs.Count; ++i) // Adjust all automated orb velocities
            {                                       // This includes idle user orbs.
                theOrbs[i].Update();
                velX += theOrbs[i].Velocity.X;
                velY += theOrbs[i].Velocity.Y;
            }

            if (theOrbs.Count > 0)      // Adjust the eye-tracking of the critters
            {
                for (int j = 0; j < puppeteers.Count; j++)
                {
                    puppeteers[j].EyeTarget = theOrbs[theOrbs.Count -1].Position;
                }
            }

            velX /= theOrbs.Count;
            velY /= theOrbs.Count;

            lblAvgVel.Text = "Avg Velocity: (" + Math.Round(velX, 2) + ", " + Math.Round(velY, 2) + ")";

            critterFrame++;
        }


        /// <summary>
        ///     This method performs most of the graphical manipulation of the canvas rendering.
        ///     It paints the orbs' trails to the background image, paints the critters via their
        ///     respective puppeteers, and renders user and auto orbs in their new positions.
        /// </summary>
        private void Render()
        {
            Bitmap bmpOrb = new Bitmap(picOrbs.Width, picOrbs.Height);
            Graphics gOrb = Graphics.FromImage(bmpOrb);

            Graphics gBack = Graphics.FromImage(picOrbs.BackgroundImage);

            // Fade the trails and pulses to black in the background
            gBack.FillRectangle(new SolidBrush(bgColor), new Rectangle(0, 0, picOrbs.Width, picOrbs.Height));

            for (int i = 0; i < theCritters.Count / 2; ++i)     // Render half of the critters in new positions and pose
            {
                puppeteers[i].GetFrame(bmpOrb, theCritters[i], critterFrame + (i * 100));
            }

            for (int i = 0; i < theOrbs.Count; ++i)     // Render orbs in new positions
            {
                gBack.DrawImage(theOrbs[i].OrbShadow, theOrbs[i].Position); // draw the trail
                gOrb.DrawImage(theOrbs[i].OrbImg, theOrbs[i].Position);     // draw the orb

                if (newPulse != -1)     // Someone wants to see their pulse! Paint it.
                {
                    Point pulsePos = new Point( theOrbs[i].Position.X - 20, theOrbs[i].Position.Y - 20 );
                    Size pulseSize = new Size( theOrbs[i].Scale + 40, theOrbs[i].Scale + 40 );
                    Rectangle pulseRect = new Rectangle( pulsePos, pulseSize );

                    gBack.FillEllipse(new SolidBrush(theOrbs[i].Colour), pulseRect);

                    newPulse = -1;
                }

                if (newCatch != -1 && puppeteers.Count > 0)     // Someone wants to catch a critter! Kill it (for now...)
                {
                    Orb hunter = new Orb(Color.White, 10);
                    hunter.ID = 0;

                    if (theOrbs[i].ID == newCatch)
                    {
                        hunter = theOrbs[i];

                        int critterIndex = GetClosestCritter(hunter.Position);

                        CritterType caughtCritter = theCritters[critterIndex];

                        SolidBrush critterBrush = new SolidBrush(caughtCritter.backColour);
                        Point critterPnt = puppeteers[critterIndex].PuppetPosition;
                        critterPnt.X -= 20;
                        critterPnt.Y -= 70;

                        Size critterSize = new Size(50, 50);
                        Rectangle critterRect = new Rectangle(critterPnt, critterSize);

                        gBack.FillEllipse(critterBrush, critterRect);

                        puppeteers.RemoveAt(critterIndex);
                        theCritters.RemoveAt(critterIndex);

                        newCatch = -1;
                    }
                }
            }

            for (int i = theCritters.Count/2 + 1; i < theCritters.Count; ++i)   // Render the other half of the critters in front
            {
                puppeteers[i].GetFrame(bmpOrb, theCritters[i], critterFrame + (i * 100));
            }

            picOrbs.Image = bmpOrb;
        }


        #region Static Methods
        /*
         *  BEWARE MERE MORTAL, FOR RACE CONDITIONS ABOUND! HERE THERE BE DRAGONS!
         */

        /// <summary>
        ///     This static method is called everytime the listener receives a new
        ///     user request. It populates a static variable for the game loop to read.
        /// </summary>
        /// <param name="id">The ID of the new user</param>
        /// <param name="clr">The user's chosen orb colour</param>
        internal static void NewOrb(int id, Color clr)
        {
            Random r = new Random(DateTime.Now.Millisecond);

            newOrb = new Orb(clr, r.Next(52, 64));
            newOrb.ID = id;
        }


        /// <summary>
        ///     This static method is called everytime the listener receives a new
        ///     movement request from a user. It populates an OrbPosition struct with
        ///     movement data to be read by the game loop.
        /// </summary>
        /// <param name="id">The ID of the user to adjust</param>
        /// <param name="x">The new X velocity</param>
        /// <param name="y">The new Y velocity</param>
        internal static void NewMove(int id, string x, string y)
        {
            newMove = new OrbPosition();
            newMove.id = id;

            Console.WriteLine(x + ", " + y);

            if (x != "" && y != "")
            {
                newMove.x = (int)Math.Round(Double.Parse(x)/10);
                newMove.y = (int)Math.Round(Double.Parse(y)/10);
            }
        }
        

        /// <summary>
        ///     This static method is called everytime the listener receives a new
        ///     pulse request from a user. It sets a static integer to the ID of the
        ///     requested pulse, to be read by the game loop.
        /// </summary>
        /// <param name="id"></param>
        internal static void NewPulse(int id)
        {
            newPulse = id;
        }


        /// <summary>
        ///     This static method is called everytime the listener receives a new
        ///     critter capture request from a user. it sets a static integer to the ID
        ///     of the requesting user, to be read by the game loop.
        /// </summary>
        /// <param name="id"></param>
        internal static void NewCatch(int id)
        {
            newCatch = id;
        }
        #endregion
    }
}
